import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { env } from "node:process";
import { generateDtsBundle } from "dts-bundle-generator";
import { type BuildOptions, build, context } from "esbuild";
import sveltePlugin from "esbuild-svelte";
import { sveltePreprocess } from "svelte-preprocess";
import pkg from "./package.json" with { type: "json" };
import {
	MODIFIERS,
	SPECIAL_MARKERS,
	functionWords,
} from "./src/generateKeys.ts";
import {
	DEFAULT_KEY_TEMPLATE,
	optionDefinitions,
} from "./src/optionDefinitions.ts";
import { wrapText } from "./src/utils.ts";

const { author, homepage, version } = pkg;

const SRC_PATH = "src";
const BUILD_PATH = join(SRC_PATH, "__generated__");
const WEB_PATH = "docs";
const CLI_BIN = env.BIBTEX_TIDY_BIN ?? join("bin", "bibtex-tidy");

/**
 * Browser features
 *
 * ----------------------------------------------------
 *                            Chrome  Edge  Safari  FF
 * ----------------------------------------------------
 * Summary/details element      12     79      6    49
 * CSS variables                49     16     10    36
 * Flexbox                      21     12     6.1   28
 * WOFF2                        35     14     10    39
 * HTML main                    26     12      7    21
 * CSS appearance: none          4     12     3.1   2
 * Template literals            41     13     9.1   34
 * let/const                    41     12     10    44
 * for of                       38     12     7     13
 * Spread operator              46     12     8     16
 * Default arguments            49     14     10    15
 * Destructuring                49     14     8     41
 * async function (svelte 5)    55     15     11    52
 * Proxy (svelte 5)             49     12     10    18
 * URLSearchParams              49     17     10.1  44
 * ----------------------------------------------------
 * Min supported                55     79     11    52
 * ----------------------------------------------------
 *
 *
 * Polyfills:
 * ----------------------------------------------------
 *                            Chrome  Edge  Safari  FF
 * ----------------------------------------------------
 * flatMap                      69     79     12    62    polyfilled by core-js
 * fromEntries                  73     79     12.1  63    polyfilled by core-js
 * trimEnd                      66     79     12    61    polyfilled by core-js
 * Nullish coalescing           80     80     13.1  72    downlevel by esbuild
 * Optional chaining            80     80     13.1  74    downlevel by esbuild
 * :where (svelte 5)            88     88     14    78    not sure if needed
 *
 */
// TODO: test on browserstack

const BROWSER_TARGETS: string[] = [
	"chrome55",
	"edge79",
	"safari11",
	"firefox52",
];

const NODE_TARGET = "node12";

const banner: string[] = [
	`bibtex-tidy v${version}`,
	"https://github.com/FlamingTempura/bibtex-tidy",
	"",
	"DO NOT EDIT THIS FILE. This file is automatically generated",
	"using `npm run build`. Edit files in './src' then rebuild.",
];

const jsBanner: string[] = [
	"/**",
	...banner.map((line) => ` * ${line}`.trimEnd()),
	" **/",
	"",
];

const webBuildOptions: BuildOptions = {
	banner: { js: jsBanner.join("\n") },
	bundle: true,
	entryPoints: ["./src/ui/index.ts"],
	keepNames: true,
	loader: { ".woff2": "file" },
	minify: true,
	// esbuild replaces the extension, e.g. js for css
	outfile: join(WEB_PATH, "bundle.js"),
	platform: "browser",
	plugins: [sveltePlugin({ preprocess: sveltePreprocess() })],
	supported: {
		// esbuild falsely identifies these as not supported for the target browsers
		"for-of": true,
		destructuring: true,
		"const-and-let": true,
		"default-argument": true,
	},
	target: BROWSER_TARGETS,
};

const jsLibBuildOptions: BuildOptions = {
	banner: { js: jsBanner.join("\n") },
	bundle: true,
	entryPoints: ["./src/index.ts"],
	keepNames: true,
	outfile: "bibtex-tidy.js",
	platform: "node",
	write: true,
	format: "esm",
};

const cliBuildOptions: BuildOptions = {
	banner: { js: `#!/usr/bin/env node\n${jsBanner.join("\n")}` },
	bundle: true,
	entryPoints: [join(SRC_PATH, "cli", "cli.ts")],
	outfile: CLI_BIN,
	platform: "node",
	sourcemap: env.NODE_ENV === "coverage" ? "inline" : false,
	sourceRoot: "./",
	target: [NODE_TARGET],
	format: "esm",
};

async function generateOptionTypes() {
	const { outputFiles } = await build({
		entryPoints: [join(SRC_PATH, "optionDefinitions.ts")],
		write: false,
		format: "esm",
		target: ["esnext"],
	});
	const outputFile = outputFiles[0];
	if (!outputFile) throw new Error("Error building options definitions");
	const bundle = new TextDecoder().decode(outputFile.contents);
	// biome-ignore lint/security/noGlobalEval: Bundle creates an export which eval doesn't know what to do with. Assign to var instead.
	const options = eval(`${bundle.replace(/^export/m, "const res = ")}; res`);

	const ts: string[] = [];

	ts.push(...jsBanner);
	ts.push("export type BibTeXTidyOptions = {");
	for (const opt of options.optionDefinitions) {
		ts.push("\t/**");
		ts.push(`\t * ${opt.title}`);
		if (opt.description) {
			ts.push("\t *");
			for (const line of opt.description) {
				ts.push(`\t * ${line}`);
			}
		}
		ts.push("\t */");
		ts.push(`\t${opt.key}?: ${opt.type};`);
	}
	ts.push("};");
	ts.push("");

	await writeFile(join(BUILD_PATH, "optionsType.ts"), ts.join("\n"));
}

async function generateVersionFile() {
	await writeFile(
		join(BUILD_PATH, "version.ts"),
		`export const version = "${version}";`,
	);
}

const NAME = `BibTeX Tidy v${version}`;
const DESCRIPTION = [
	"Cleaner and formatter for BibTeX files.",
	"",
	"If no input or output file is specified, bibtex-tidy reads the standard input or writes to the standard output respectively. Use -m to overwrite the input file.",
];

const SYNOPSIS = "bibtex-tidy [infile] [-o outfile] [option...]";

async function generateCLIHelp() {
	const help: string[] = [
		`Usage: ${SYNOPSIS}`,
		"",
		NAME,
		"=".repeat(NAME.length),
		...DESCRIPTION.map((d) => wrapText(d, 84).join("\n")),
		"",
		"Options:",
		...formatOptions(2, 84),
		`Full documentation <${homepage}>`,
	];
	await writeFile(
		join(BUILD_PATH, "manPage.ts"),
		`${jsBanner.join("\n")}export const manPage = ${JSON.stringify(help, null, "\t")};`,
	);
}

async function generateManPage() {
	await writeFile(
		"bibtex-tidy.0",
		[
			"NAME",
			`    ${NAME}`,
			"",
			"SYNOPSIS",
			`    ${SYNOPSIS}`,
			"",
			"DESCRIPTION",
			...DESCRIPTION.map((d) =>
				wrapText(d, 61)
					.map((line) => `    ${line}`)
					.join("\n"),
			),
			"",
			"OPTIONS",
			...formatOptions(4, 65),
			"BUGS",
			`    ${homepage}`,
			"",
			"AUTHOR",
			`    ${author}`,
		].join("\n"),
	);
}

async function generateReadme() {
	const readme = await readFile("README.md", "utf8");
	await writeFile(
		"README.md",
		readme.replace(
			/```manpage.*?```/s,
			`\`\`\`manpage\n${formatOptions(2, 84).join("\n")}\n\`\`\``,
		),
	);
}

async function generateKeyTemplatePage() {
	let doc = await readFile("docs/manual/key-generation.html", "utf8");
	const markers = Object.entries(SPECIAL_MARKERS).map(
		([k, { description }]) => `<li><code>[${k}]</code>: ${description}</li>`,
	);
	const modifiers = Object.entries(MODIFIERS).map(
		([k, { description }]) => `<li><code>:${k}</code>: ${description}</li>`,
	);

	doc = doc
		.replace(
			/<!--DEFAULT_KEY-->.*?<!--END-->/s,
			`<!--DEFAULT_KEY-->${DEFAULT_KEY_TEMPLATE}<!--END-->`,
		)
		.replace(
			/<!--MARKERS-->.*?<!--END-->/s,
			`<!--MARKERS-->${markers.join("\n")}<!--END-->`,
		)
		.replace(
			/<!--MODIFIERS-->.*?<!--END-->/s,
			`<!--MODIFIERS-->${modifiers.join("\n")}<!--END-->`,
		)
		.replace(
			/<!--WORDS-->.*?<!--END-->/s,
			`<!--WORDS-->${[...functionWords].join(", ")}<!--END-->`,
		);
	await writeFile("docs/manual/key-generation.html", doc);
}

function formatOptions(indent: number, lineWidth: number): string[] {
	return optionDefinitions.flatMap((opt) => {
		if (opt.deprecated) return [];

		const description: string[] = [];

		if (opt.description) {
			description.push(...opt.description.flatMap((line) => [line, "\n"]));
		}

		if (opt.examples && opt.examples.length > 0) {
			description.push(
				"Examples:",
				opt.examples.filter((example) => example).join(", "),
				"",
			);
		}

		return [
			Object.keys(opt.cli).join(", "),
			...description.flatMap((line) =>
				wrapText(line, lineWidth - indent - 4).map((line) => `    ${line}`),
			),
		].map((line) => `${" ".repeat(indent)}${line}`);
	});
}

async function buildJSBundle() {
	console.time("JS bundle built");
	await build(jsLibBuildOptions);
	console.timeEnd("JS bundle built");
}

async function buildTypeDeclarations() {
	console.time("Type declarations");
	const typeFile = generateDtsBundle([
		{ filePath: "./src/index.ts", output: { noBanner: true } },
	])[0];
	if (!typeFile) throw new Error("Failed to generate type file");
	await writeFile("bibtex-tidy.d.ts", typeFile);
	console.timeEnd("Type declarations");
}

async function buildCLI() {
	console.time("CLI built");
	await build(cliBuildOptions);
	await chmod(CLI_BIN, 0o755); // rwxr-xr-x
	console.timeEnd("CLI built");
}

async function buildWebBundle() {
	console.time("Web bundle built");
	await build(webBuildOptions);
	console.timeEnd("Web bundle built");
}

async function serveWeb() {
	const ctx = await context({
		...webBuildOptions,
		sourcemap: true,
		write: false,
		plugins: [sveltePlugin({ preprocess: sveltePreprocess() })],
	});
	const server = await ctx.serve({ servedir: WEB_PATH });
	console.log(`Access on http://localhost:${server.port}`);
}

if (process.argv.includes("--serve")) {
	serveWeb();
} else {
	mkdir(BUILD_PATH, { recursive: true })
		.then(() =>
			Promise.all([
				generateOptionTypes(),
				generateVersionFile(),
				generateManPage(),
				generateKeyTemplatePage(),
				generateCLIHelp(),
				generateReadme(),
			]),
		)
		.then(() =>
			Promise.all([
				!process.argv.includes("--no-defs")
					? buildTypeDeclarations()
					: undefined,
				buildJSBundle(),
				buildCLI(),
				buildWebBundle(),
			]),
		);
}
